HTMLのaタグとimgタグをMarkdown記法に変換するシェルスクリプトを書いてみた
HTMLのタグをMarkdown記法に変換したい
こんにちは、のんピ(@non____97)です。
皆さんはHTMLのタグをMarkdown記法に変更したいなと思ったことはありますか? 私はあります。
私は基本的にMarkdown記法をベースに記事を書いています。しかし、一部直接HTMLのaタグやimgタグを使っている場面があります。そんな折、Zenn記法に記事を変換する必要が出てきました。
Zennでは直接HTMLタグを使用することはできません。ZennのMarkdown記法は以下をご覧ください。
HTMLをMarkdownに変換するだけであればPandocを使用すれば良いと思います。Pandocの紹介と使い方は以下記事とユーザーガイドをご覧ください。
今回はZenn固有の記法もあるため、Pandocではなくsedとawkで色々カスタマイズを頑張ってみたくなってきました。
ということでやってみます。
やってみた
用意したスクリプト
使い方
使い方は以下のとおりです。変換したいファイル名と変換後のファイルに付与するサフィックス、変換後のファイルの出力先のディレクトリを指定します。
$ bash convert-wp-to-zenn/index.sh --help
Usage: index.sh [OPTIONS] <input_markdown_file>
Options:
-h, --help Show this help message and exit
-s, --suffix SUFFIX Specify the output file suffix (default: _conversion)
-d, --output-dir DIRECTORY Specify the output directory (default: same as input file)
ディレクトリツリー
ディレクトリツリーは以下のとおりです。元々はWordPressの書き方からからZenn記法に変換したかったという背景があるのでconvert-wp-to-zenn
としています。お好みの名前でどうぞ。
tree
.
├── articles
│ └── <ここに記事を配置>
└── convert-wp-to-zenn
├── index.sh
└── lib
├── converters.sh
└── utils.sh
4 directories, 4 files
./convert-wp-to-zenn/index.sh
エントリーとなる./convert-wp-to-zenn/index.sh
では以下の処理を行っています。
- ライブラリのシェルスクリプトの読み込み
- 引数のパース
- 入力ファイルの検証
- 出力ファイル名の設定
- 必要に応じて出力先ディレクトリの作成
- Markdownへの変換
- 変換後のファイルの文字数カウント
- 使用方法の表示
コードは以下のとおりです。
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# 定数定義
readonly SCRIPT_NAME="${0##*/}"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LIB_DIR="${SCRIPT_DIR}/lib"
readonly DEFAULT_OUTPUT_SUFFIX="_conversion"
# ライブラリの読み込み
for lib in "${LIB_DIR}"/*.sh; do
# shellcheck source=./lib/utils.sh
# shellcheck source=./lib/converters.sh
source "$lib"
done
# メイン処理
main() {
local input_file
local output_file
local output_suffix="$DEFAULT_OUTPUT_SUFFIX"
local output_dir=""
parse_arguments "$@"
validate_input_file "$input_file"
setup_output_file "$input_file" "$output_suffix" "$output_dir"
log_info "Starting conversion process for $input_file..."
if ! convert_markdown "$input_file" "$output_file"; then
die "Conversion process failed for $input_file"
fi
local char_count
char_count=$(get_char_count "$output_file")
log_info "Conversion completed successfully."
log_info "Output file: $output_file"
log_info "Character count: $char_count"
}
# 引数のパース
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
-s | --suffix)
if [[ -n "$2" ]]; then
output_suffix="$2"
shift 2
else
die "Error: Argument for $1 is missing"
fi
;;
-d | --output-dir)
if [[ -n "$2" ]]; then
output_dir="$2"
shift 2
else
die "Error: Argument for $1 is missing"
fi
;;
-*)
die "Unknown option: $1"
;;
*)
if [[ -z ${input_file:-} ]]; then
input_file="$1"
else
die "Error: Multiple input files specified"
fi
shift
;;
esac
done
if [[ -z ${input_file:-} ]]; then
usage
die "Error: Input file not specified"
fi
}
# 使用方法の表示
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <input_markdown_file>
Options:
-h, --help Show this help message and exit
-s, --suffix SUFFIX Specify the output file suffix (default: $DEFAULT_OUTPUT_SUFFIX)
-d, --output-dir DIRECTORY Specify the output directory (default: same as input file)
EOF
}
# 入力ファイル検証
validate_input_file() {
local file="$1"
if ! [[ -f "$file" && -r "$file" ]]; then
die "Input file not found or not readable: $file"
fi
}
# 出力ファイル名の設定
setup_output_file() {
local input="$1"
local suffix="$2"
local dir="$3"
local input_dir
local input_filename
input_dir=$(dirname "$input")
input_filename=$(basename "$input")
if [[ -z "$dir" ]]; then
dir="$input_dir"
fi
output_file="${dir}/${input_filename%.*}${suffix}.md"
# 出力ディレクトリが存在しない場合は作成
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir" || die "Failed to create output directory: $dir"
fi
# 既に同名のファイルがある場合は警告
if [[ -e "$output_file" ]]; then
log_warn "Output file already exists and will be overwritten: $output_file"
fi
}
# 文字数のカウント
get_char_count() {
local file="$1"
wc -m <"$file" | tr -d '[:space:]'
}
# スクリプトが直接実行された場合のみメイン処理を実行
# source で呼び出された時に実行されないように
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi
./convert-wp-to-zenn/lib/utils.sh
主にログ出力を行う./convert-wp-to-zenn/lib/utils.sh
は以下のとおりです。
#!/usr/bin/env bash
# 定数定義
readonly ANSI_RED='\033[0;31m'
readonly ANSI_GREEN='\033[0;32m'
readonly ANSI_YELLOW='\033[0;33m'
readonly ANSI_RESET='\033[0m'
# ログレベルの定義
declare -rA LOG_LEVELS=(
[DEBUG]=0
[INFO]=1
[WARN]=2
[ERROR]=3
)
# デフォルトのログレベルを設定
: "${LOG_LEVEL:=INFO}"
# ログ出力関数
log() {
local -r level="$1"
local -r message="$2"
local -r timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
if [[ ! -v "LOG_LEVELS[$level]" ]]; then
printf "Invalid log level: %s\n" "$level" >&2
return 1
fi
if [[ ${LOG_LEVELS[$level]} -ge ${LOG_LEVELS[$LOG_LEVEL]} ]]; then
local color
case "$level" in
DEBUG) color="$ANSI_RESET" ;;
INFO) color="$ANSI_GREEN" ;;
WARN) color="$ANSI_YELLOW" ;;
ERROR) color="$ANSI_RED" ;;
esac
printf "${color}[%s] [%s] %s${ANSI_RESET}\n" "$timestamp" "$level" "$message" >&2
fi
}
# ログレベルごとのログ出力関数のエイリアス
log_debug() { log DEBUG "$1"; }
log_info() { log INFO "$1"; }
log_warn() { log WARN "$1"; }
log_error() { log ERROR "$1"; }
# エラー終了時の処理
die() {
log_error "$1"
exit 1
}
./convert-wp-to-zenn/lib/converters.sh
実際のMarkdown記法に変換する処理を行う./convert-wp-to-zenn/lib/converters.sh
では以下を行っています。
- はてなブログカードのiframeの変換
- aタグの変換
- imgタグの変換
- pタブのalertクラスの変換
なお、一行単位で処理を行っているので処理対象が複数行に跨る場合は処理はできません。
コードは以下のとおりです。
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# 変換処理の実行
convert_markdown() {
local -r input_file="$1"
local -r output_file="$2"
{
convert_hatenablogcard <"$input_file" |
convert_a_tags |
convert_img_tags |
convert_alert_class >"$output_file"
} || die "Conversion process failed"
}
# hatenablogcard iframeの変換
convert_hatenablogcard() {
sed -E 's/<iframe class="hatenablogcard"[^>]*src="[^?]*\?url=([^"]*)"[^>]*><\/iframe>/\1/'
}
# aタグの変換
convert_a_tags() {
sed -E 's/<a ([^>]*href="([^"]*)"[^>]*)>([^<]*)<\/a>/[\3](\2)/g'
}
# imgタグの変換
convert_img_tags() {
awk '
{
while (match($0, /<img [^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/)) {
src = substr($0, RSTART, RLENGTH)
sub(/.*src="/, "", src)
sub(/".*/, "", src)
alt = substr($0, RSTART, RLENGTH)
sub(/.*alt="/, "", alt)
sub(/".*/, "", alt)
replacement = "![" alt "](" src ")"
$0 = substr($0, 1, RSTART-1) replacement substr($0, RSTART+RLENGTH)
}
print
}
'
}
# alertクラスの変換
convert_alert_class() {
sed -E 's/<p class="alert">(.*)<\/p>/:::message\n\1\n:::/'
}
この記事のタイトルで紹介しているaタグとimgタグの変換を行っている箇所についてはもう少し補足をします。
aタグはsedを使用して置換します。
パターンは<a ([^>]*href="([^"]*)"[^>]*)>([^<]*)<\/a>
です。いくつかのチャンクで分割して説明します。
<a
:<a
とaタグの先頭とマッチ([^>]*href="([^"]*)"[^>]*)
: href属性を含むタグの属性部分にマッチ[^>]*
: 0回以上の>
以外の文字href="([^"]*)"
: href属性とその値とマッチ (([^"]*)
でURL部分を取得する)[^>]*
: 0回以上の>
以外の文字
>
: aタグの末尾にマッチ([^<]*)
: タグの内容 = アンカーテキストを取得<\/a>
: aタグの終了タグにマッチ
置換パターンは[\3](\2)
です。それぞれMarkdownの以下を示しています。
[\3]
: アンカーテキスト(\2)
: URL
imgタグはawkを使用して置換します。コメントで補足しました。
awk '
{
# 各行($0)に対して、想定しているimgタグにマッチするものを探す
while (match($0, /<img [^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/)) {
# マッチしたimgタグ全体を取得
src = substr($0, RSTART, RLENGTH)
# src属性の値だけを取得
sub(/.*src="/, "", src)
sub(/".*/, "", src)
# マッチしたimgタグ全体を取得
alt = substr($0, RSTART, RLENGTH)
# alt属性の値だけを取得
sub(/.*alt="/, "", alt)
sub(/".*/, "", alt)
# Markdown形式に組み立て
replacement = "![" alt "](" src ")"
$0 = substr($0, 1, RSTART-1) replacement substr($0, RSTART+RLENGTH)
}
# 出力
print
}
'
実行してみる
以下のようなファイルを用意しました。
<!-- はてなブログカードのiframeの変換 -->
<iframe class="hatenablogcard" src="https://hatenablog-parts.com/embed?url=https://aws.amazon.com/jp/blogs/news/new-aws-public-ipv4-address-charge-public-ip-insights/" width="680" height="150" frameborder="0" scrolling="no"></iframe>
<iframe class="hatenablogcard" frameborder="0" src="https://hatenablog-parts.com/embed?url=https://dev.classmethod.jp/articles/amazon-fsx-netapp-ontap-bluexp-flexcache-volume/" scrolling="no"></iframe>
<!-- aタグの変換 -->
こんにちは、のんピ(<a href="https://twitter.com/non____97" target="_blank" rel="noreferrer">@non____97</a>)です。
こんにちは、のんピ2(<a target="_blank" href="https://twitter.com/non____97" >ブログ狂中年卍</a>)です。
[DevelopersIO](https://dev.classmethod.jp/)
<!-- imgタグの変換 -->
<img src="https://images.ctfassets.net/ct0aopd36mqt/wp-thumbnail-dcbaa2123fec72b4fafe12edd4285aaf/178121bca8a805d8af9e86e4e4bbe732/aws-cloudformation" alt="CloudFormation"/>
<img alt="CloudFormation" width="651" height="451" src="https://images.ctfassets.net/ct0aopd36mqt/wp-thumbnail-dcbaa2123fec72b4fafe12edd4285aaf/178121bca8a805d8af9e86e4e4bbe732/aws-cloudformation"/>
<!-- pタブのalertクラスの変換 -->
<p class="alert">アラートです。 これは。</p>
<p class="alert">アラートです。 これは2。</p>
このファイルに対して処理をかけます。
$ bash convert-wp-to-zenn/index.sh articles/test.txt
[2024-07-14 14:33:30] [INFO] Starting conversion process for articles/test.txt...
[2024-07-14 14:33:30] [INFO] Conversion completed successfully.
[2024-07-14 14:33:30] [INFO] Output file: articles/test_conversion.md
[2024-07-14 14:33:30] [INFO] Character count: 855
出力されたファイルは以下のとおりです。
<!-- はてなブログカードのiframeの変換 -->
https://aws.amazon.com/jp/blogs/news/new-aws-public-ipv4-address-charge-public-ip-insights/
https://dev.classmethod.jp/articles/amazon-fsx-netapp-ontap-bluexp-flexcache-volume/
<!-- aタグの変換 -->
こんにちは、のんピ([@non____97](https://twitter.com/non____97))です。
こんにちは、のんピ2([ブログ狂中年卍](https://twitter.com/non____97))です。
[DevelopersIO](https://dev.classmethod.jp/)
<!-- imgタグの変換 -->
![CloudFormation](https://images.ctfassets.net/ct0aopd36mqt/wp-thumbnail-dcbaa2123fec72b4fafe12edd4285aaf/178121bca8a805d8af9e86e4e4bbe732/aws-cloudformation)
<img alt="CloudFormation" width="651" height="451" src="https://images.ctfassets.net/ct0aopd36mqt/wp-thumbnail-dcbaa2123fec72b4fafe12edd4285aaf/178121bca8a805d8af9e86e4e4bbe732/aws-cloudformation"/>
<!-- pタブのalertクラスの変換 -->
:::message
アラートです。 これは。
:::
:::message
アラートです。 これは2。
:::
意図したとおりに変換されていますね。2つ目のimgタグが変換されていないのは属性の順番がawkのパターンとマッチしていないためです。
出力先のディレクトリパスとサフィックスを指定して2回実行します。
$ bash convert-wp-to-zenn/index.sh articles/test.txt -d dst -s _suffix
[2024-07-14 14:36:17] [INFO] Starting conversion process for articles/test.txt...
[2024-07-14 14:36:17] [INFO] Conversion completed successfully.
[2024-07-14 14:36:17] [INFO] Output file: dst/test_suffix.md
[2024-07-14 14:36:17] [INFO] Character count: 855
$ ls dst/
test_suffix.md
$ bash convert-wp-to-zenn/index.sh articles/test.txt -d dst -s _suffix
[2024-07-14 14:36:37] [WARN] Output file already exists and will be overwritten: dst/test_suffix.md
[2024-07-14 14:36:37] [INFO] Starting conversion process for articles/test.txt...
[2024-07-14 14:36:37] [INFO] Conversion completed successfully.
[2024-07-14 14:36:37] [INFO] Output file: dst/test_suffix.md
[2024-07-14 14:36:37] [INFO] Character count: 855
正常に実行されていますね。警告も表示されます。
シェルスクリプトで頑張りたい時に
HTMLのaタグとimgタグをMarkdown記法に変換するシェルスクリプトを書いてみました。
シェルスクリプトで頑張りたい時に参考にしてみてください。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!